﻿/*
 * This is part of the dotDiff Project
 * 
 * Author: David Rundle (http://www.dbones.co.uk)
 * Project Home: http://www.codeplex.com/DotDiff
 * 
 */

#region Using

using System;
using System.IO;
using System.Text;
using System.Xml;

#endregion

namespace DotDiff
{

    /// <summary>
    /// this is a collection of XML functions
    /// </summary>
    public class XmlFunctions
    {
        //private string idNode;
        //private XmlNamespaceManager nsMgr;
        //private bool xpathPosition;

        private XmlSettings xmlSettings;

        ///<summary>Initializes a new instance of the <see cref="T:DotDiff.XmlFunctions"></see> class.</summary>
        /// ///<param name="nsMgr">the NameSpace manager</param>
        [Obsolete("please use the XmlSettigns Constructor", false)]
        public XmlFunctions(XmlNamespaceManager nsMgr)
        {
            xmlSettings = new XmlSettings();
            xmlSettings.NsMgr = nsMgr;
        }

        ///<summary>Initializes a new instance of the <see cref="T:DotDiff.XmlFunctions"></see> class.</summary>
        ///<param name="idNode">The IdNode if there is one, else pass in "" or null</param>
        ///<param name="nsMgr">the NameSpace manager</param>
        [Obsolete("please use the XmlSettigns Constructor", false)]
        public XmlFunctions(string idNode, XmlNamespaceManager nsMgr)
        {
            xmlSettings = new XmlSettings();
            xmlSettings.IdNode = idNode;
            xmlSettings.NsMgr = nsMgr;
        }

        ///<summary>Initializes a new instance of the <see cref="T:DotDiff.XmlFunctions"></see> class.</summary>
        ///<param name="idNode">The IdNode if there is one, else pass in "" or null</param>
        ///<param name="nsMgr">the NameSpace manager</param>
        ///<param name="xmlPosstion">weather to use node posistioning on these XML functions.</param>
        [Obsolete("please use the XmlSettigns Constructor", false)]
        public XmlFunctions(string idNode, bool xmlPosstion, XmlNamespaceManager nsMgr)
        {
            xmlSettings = new XmlSettings();
            xmlSettings.IdNode = idNode;
            xmlSettings.NsMgr = nsMgr;
            xmlSettings.XpathPosition = xmlPosstion;
        }

        ///<summary>Initializes a new instance of the <see cref="T:DotDiff.XmlFunctions"></see> class.</summary>
        ///<param name="idNode">The IdNode if there is one, else pass in "" or null</param>
        ///<param name="nsMgr">the NameSpace manager</param>
        ///<param name="xmlPosstion">weather to use node posistioning on these XML functions. (Xpath)</param>
        /// <param name="xmlAttributes">weather to use node attributes on these XML functions. (Xpath)</param>
        [Obsolete("please use the XmlSettigns Constructor", false)]
        public XmlFunctions(string idNode, bool xmlPosstion, bool xmlAttributes, XmlNamespaceManager nsMgr)
        {
            xmlSettings = new XmlSettings();
            xmlSettings.IdNode = idNode;
            xmlSettings.NsMgr = nsMgr;
            xmlSettings.XpathPosition = xmlPosstion;
            xmlSettings.XpathAttributes = xmlAttributes;
        }

        /// <summary>
        /// this will create an instance of the XmlFunctions using the XmlSettings passed in
        /// </summary>
        /// <param name="xmlSettings">XmlSettings to use.</param>
        public XmlFunctions(XmlSettings xmlSettings)
        {
            this.xmlSettings = xmlSettings;
        }

        /// <summary>
        /// The XmlSettings used on these functions.
        /// </summary>
        public XmlSettings XmlSettings
        {
            get
            {
                return this.xmlSettings;
            }
            set
            {
                this.xmlSettings = value;
            }
        }



        /// <summary>
        /// this will return true if both sides have the same attributes
        /// amd value for those attributes.
        /// </summary>
        /// <param name="left">node 1</param>
        /// <param name="right">node 2</param>
        /// <returns>If they have the same attributes</returns>
        public bool CompareAttributes(XmlNode left, XmlNode right)
        {
            if (left.Attributes.Count != right.Attributes.Count)
            {
                return false;
            }
            foreach (XmlAttribute attribute in right)
            {
                //make sure they have the same elements and vales.
                if (left.Attributes[attribute.Name] == null)
                {
                    return false;
                }
                if (left.Attributes[attribute.Name].Value != attribute.Value)
                {
                    return false;
                }
            }
            return true;
        }

        /// <summary>
        /// copies accross the attributes from one node to another
        /// </summary>
        /// <param name="toCopy">the attributes to copy</param>
        /// <param name="CopyTo">the node to copy the attributes to</param>
        public void CopyAttributes(XmlNode toCopy, XmlNode CopyTo)
        {
            XmlDocument doc = CopyTo.OwnerDocument;
            foreach (XmlAttribute attr in toCopy.Attributes)
            {
                XmlAttribute newAttr = doc.CreateAttribute(attr.Name, attr.NamespaceURI);
                newAttr.Value = attr.Value;
                CopyTo.Attributes.Append(newAttr);
            }
        }

        //TODO: look at copying the node to a certain location.

        /// <summary>
        /// This will copy an entire node, including namespaces, attributes and values,
        /// to another node. (can be used to copy XMLNode to an XmlDoc)
        /// </summary>
        /// <param name="toCopy">the node to copy</param>
        /// <param name="CopyTo">the parent node to append the copied node to (AppendChild)</param>
        public void CopyEntireNode(XmlNode toCopy, XmlNode CopyTo)
        {
            //create a new node and attach this to the document.
            XmlDocument doc = CopyTo.OwnerDocument;
            XmlNode newNode = doc.CreateElement(toCopy.LocalName, toCopy.NamespaceURI);
            //only copy text on a value node.
            if (IsValue(toCopy))
                newNode.InnerText = toCopy.InnerText;
            CopyTo.AppendChild(newNode);

            //get the attributes
            CopyAttributes(toCopy, newNode);

            foreach (XmlNode childNode in toCopy.ChildNodes)
            {
                //only process the elements
                if (childNode.GetType() != typeof(XmlElement))
                    continue;
                CopyEntireNode(childNode, newNode);
            }
        }

        /// <summary>
        /// this will create a Skeleton structure
        /// </summary>
        /// <param name="toCopy">the node to copy, and all its parent nodes, this will not include any siblings, just the empty node</param>
        /// <param name="docTo">the document to copy too</param>
        public void CopyNode(XmlNode toCopy, XmlDocument docTo)
        {
            XmlNode current = docTo.DocumentElement;
            string idNode = xmlSettings.IdNode;

            //find the required structure
            string xPath = GetXPath(toCopy);
            string[] nodes = xPath.Split('/');

            //nodes[1] = "";

            XmlNode ParentNode = docTo.DocumentElement;
            XmlDocument docFrom = toCopy.OwnerDocument;

            //reset ToCopy to point at its Document root
            toCopy = docFrom.DocumentElement;
            int index = 1;

            string find = "";


            for (int i = 1; i < nodes.Length - 1; i++)
            {
                index = xPath.IndexOf("/", index);
                find = xPath.Substring(0, index);
                index++;
                if (current == null)
                {
                    //get node to add
                    toCopy = docFrom.SelectSingleNode(find);   //GetChildElement(toCopy, nodes[i]);  //toCopy.SelectSingleNode("./" + nodes[i]);

                    //add in the XML as it does not exist
                    current = docTo.CreateElement(toCopy.LocalName, toCopy.NamespaceURI);
                    if (!string.IsNullOrEmpty(idNode))
                    {
                        XmlNode id = GetChildElement(toCopy, idNode);
                        if (id != null)
                        {
                            CopyEntireNode(id, current);
                        }
                    }

                    //append to the parent node
                    if (ParentNode == null)
                    {
                        docTo.AppendChild(current);
                    }
                    else
                    {
                        ParentNode.AppendChild(current);
                    }

                    //get the attributes
                    CopyAttributes(toCopy, current);

                }
                //set the parent node.
                ParentNode = current;
                current = GetChildElement(current, nodes[i + 1]);

            }
            //return docTo;
        }

        /// <summary>
        /// This will copy an entire node, including namespaces, attributes and values,
        /// to another node. (can be used to copy XMLNode to an XmlDoc)
        /// </summary>
        /// <param name="toCopy">the node to copy</param>
        /// <param name="CopyTo">the parent node to append the copied node to (AppendChild)</param>
        public void CopyNodeWithId(XmlNode toCopy, XmlNode CopyTo)
        {
            //create a new node and attach this to the document.
            XmlDocument doc = CopyTo.OwnerDocument;
            XmlNode newNode = doc.CreateElement(toCopy.LocalName, toCopy.NamespaceURI);
            XmlNode id = GetChildElement(toCopy, "id");
            if (id != null)
            {
                CopyEntireNode(id, newNode);
            }
            CopyTo.AppendChild(newNode);

            //get the attributes
            CopyAttributes(toCopy, newNode);
        }

        /// <summary>
        /// Deletes a node
        /// </summary>
        /// <param name="toDelete">node to remove</param>
        public void DeleteNode(XmlNode toDelete)
        {
            if (toDelete == null)
                return;

            //delete the entire document
            if (toDelete.OwnerDocument.DocumentElement == toDelete)
            {
                toDelete.OwnerDocument.InnerXml = "";
            }
            else //remove the single node.
            {
                toDelete.ParentNode.RemoveChild(toDelete);
            }

        }

        /// <summary>
        /// Deletes a node
        /// </summary>
        /// <param name="doc">the containing document</param>
        /// <param name="xpath">the Xpath</param>
        public void DeleteNode(XmlDocument doc, string xpath)
        {
            XmlNode toDelete = doc.SelectSingleNode(xpath);

            if (toDelete != null)
            {
                toDelete.ParentNode.RemoveChild(toDelete);
            }
        }

        /// <summary>
        /// this will return true if the current element contains
        /// a child node
        /// </summary>
        /// <param name="current">the current node</param>
        /// <param name="childName">the name of the child element</param>
        /// <returns>true if it contains the child element.</returns>
        public XmlNode GetChildElement(XmlNode current, string childName)
        {
            string xpath = string.Format("./{0}", childName);
            return current.SelectSingleNode(xpath);
        }

        /// <summary>
        /// finds the line number of a node.
        /// </summary>
        /// <param name="findNodeLineNo">the line number</param>
        /// <param name="xml">the xml</param>
        /// <returns></returns>
        public int GetLineNumber(XmlNode findNodeLineNo, string xml)
        {


            bool found = false;

            int count = 0;
            using (StringReader sr = new StringReader(xml))
            {
                XmlReaderSettings settings = new XmlReaderSettings();
                settings.LineNumberOffset = 0;
                settings.LinePositionOffset = 0;


                XmlReader xr = XmlReader.Create(sr, settings);
                IXmlLineInfo lineInfo = (IXmlLineInfo)xr;


                while (xr.Read())
                {
                    if (xr.NodeType == XmlNodeType.Element)
                    {
                        if (xr.LocalName == findNodeLineNo.LocalName)
                        {
                            //see if the xpaths are the same!!!
                            XmlDocument doc = new XmlDocument();
                            doc.InnerXml = xml;
                            int current = 0;
                            XmlNode node = GetNodeFromCount(count, ref current, doc.DocumentElement);
                            if (node != null)
                            {
                                if (GetXPath(node) == GetXPath(findNodeLineNo))
                                {
                                    found = true;
                                    count = lineInfo.LineNumber;
                                    break;

                                }
                            }
                        }//if (xr.LocalName == findNodeLineNo.LocalName)
                        count++;
                    }
                }
            }

            if (found)
            {
                return count;
            }

            return -1;
        }

        /// <summary>
        /// this will return the possition of the node. 0 if it is not part of a collection.
        /// </summary>
        /// <param name="current">the node to find the possion of</param>
        /// <returns>an int which is 1 based. to co-inside with Xpath</returns>
        public int GetNodePosition(XmlNode current)
        {
            if (current == current.OwnerDocument.DocumentElement)
                return 0;
            
            XmlNode parent = current.ParentNode;
            if (parent == null) //has no parent, thus no sibling
                return 0;
            
            if (parent.ChildNodes.Count == 1) //only sibling, no point in the possion
                return 0;

            //does not work
            //for (int i = 0; i < parent.ChildNodes.Count; i++)
            //{
            //    if (parent.ChildNodes[i] == current)
            //    {
            //        return i + 1; //found.
            //    }
            //}

            //should get the correct possistion
            XmlElement temp = (XmlElement) parent;
            int index = 1;
            foreach (XmlNode node in temp.GetElementsByTagName(current.Name))
            {
                if (node == current)
                {
                    return index;
                }
                index++;
            }
            return 0; //not found.
        }

        /// <summary>
        /// Gets the Xpath for the Parent node
        /// </summary>
        /// <param name="currentXPath">the Xpath for the current node</param>
        /// <returns>an Xpath string</returns>
        public string GetParentXPath(string currentXPath)
        {
            int last = currentXPath.LastIndexOf("/");
            return currentXPath.Substring(0, last);
        }

        ///// <summary>
        ///// this will genterate an absolute Xpath for a node.
        ///// </summary>
        ///// <returns>the XPATH for that node.</returns>
        //public string GetXPath(XmlNode current)
        //{
        //    return GetXPath(current, nsMgr);
        //}

        /// <summary>
        /// This will get the Xpath for the element located at a line number.
        /// </summary>
        /// <param name="lineNumber">the line number</param>
        /// <param name="xml">the xml</param>
        /// <returns></returns>
        public string GetXPath(int lineNumber, string xml)
        {

            bool found = false;
            int count = 0;
            using (StringReader sr = new StringReader(xml))
            {
                XmlReaderSettings settings = new XmlReaderSettings();
                settings.LineNumberOffset = 0;
                settings.LinePositionOffset = 0;


                XmlReader xr = XmlReader.Create(sr, settings);
                IXmlLineInfo lineInfo = (IXmlLineInfo)xr;

                while (xr.Read())
                {
                    if (lineInfo.LineNumber >= lineNumber)
                    {
                        found = true;
                        break;
                    }
                    if (xr.NodeType == XmlNodeType.Element)
                    {
                        count++;
                    }
                }
            }

            if (found)
            {
                XmlDocument doc = new XmlDocument();
                doc.InnerXml = xml;
                int current = 0;
                XmlNode node = GetNodeFromCount(count, ref current, doc.DocumentElement);
                if (node != null)
                {
                    return GetXPath(node);
                }
            }

            return "";
        }

        //TODO: require to check the cell posistioning! and attributes
        //1. find the Origonal Xpath, then see if the Possistion Matches.
        //if Orginonal does not exist then its missing
        //if it does but the Xpath is Different then the pos is a diff Type?

        /// <summary>
        /// this will genterate an absolute Xpath for a node. 
        /// </summary>
        /// <returns>the XPATH for that node.</returns>
        public string GetXPath(XmlNode current)
        {
            StringBuilder path = new StringBuilder();
            XmlNode NodePointer = current; //this is used as a pointer.

            while (NodePointer.Name != "#document")
            {
                //get node info
                string nodeName = GetNodeWithPrefix(NodePointer);
                string attributes = xmlSettings.XpathAttributes ? GetXPathAttributes(NodePointer): "";
                string posistion = xmlSettings.XpathPosition ? GetXPathPosition(NodePointer) : "";
                //prefix onto the Xpath string.
                path.Insert(0, "/" + nodeName + attributes + posistion);
                NodePointer = NodePointer.ParentNode;
            }

            return path.ToString();
        }

        /// <summary>
        /// is a node which will contain many object node. (added as I noticed InfoPath has this style.)
        /// </summary>
        /// <param name="current">node to test</param>
        /// <returns></returns>
        [Obsolete("No longer in use", true)]
        public bool IsGroup(XmlNode current)
        {
            if (current.ChildNodes.Count == 1)
            {
                if (!IsValue(current) && !IsObject(current))
                {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// This is a simple function, to test if the current
        /// node is an object like a book, holding many values or child objects.
        /// </summary>
        /// <param name="current">the current node</param>
        public bool IsObject(XmlNode current)
        {
            if (current.ChildNodes.Count >= 1)
            {
                if (current.ChildNodes.Count == 1)
                {
                    return !IsValue(current);
                }
                return true;
            }
            return false;
        }

        /// <summary>
        /// this will test if the current node is a value type.
        /// </summary>
        /// <param name="current"></param>
        /// <returns></returns>
        public bool IsValue(XmlNode current)
        {
            XmlNode child = current.ChildNodes[0];
            if (child == null)
            {
                return true;
            }
            if (child.GetType() == typeof(XmlText))
            {
                return true;
            }
            return false;
        }

        /// <summary>
        /// gets a child node via its name.
        /// </summary>
        /// <param name="current">if null, it will use the document element. ELSE provide the current node to search within</param>
        /// <param name="name">name of the node to find</param>
        /// <returns>the found node or null</returns>
        private XmlNode GetChildNodeByName(XmlNode current, string name)
        {
            if (current == null)
                return null;

            XmlNode toScan = current;

            //(child.Attributes == null)
            //loop through all the child nodes.
            foreach (XmlNode child in toScan.ChildNodes)
            {

                if (child.Attributes["name"] != null) //(child.Attributes.Count > 0)
                {
                    if (child.Attributes["name"].Value == name)
                    {
                        return child;
                    }
                }
                XmlNode node = GetChildNodeByName(child, name);
                if (node != null)
                {
                    return node;
                }
            }

            return null;
        }

        /// <summary>
        /// This will hunt down the node at the Nth posistion! (recurive have to find better way.)
        /// </summary>
        /// <param name="count">the posistion</param>
        /// <param name="current">the current location, just set this to 0</param>
        /// <param name="node">the current Xml node, set this to the document node</param>
        /// <returns>the node at the nth pos</returns>
        private XmlNode GetNodeFromCount(int count, ref int current, XmlNode node)
        {
            //only process the elements
            if (node.GetType() != typeof(XmlElement))
                return null;


            current++;
            if (current == count + 1)
                return node;

            foreach (XmlNode childNode in node.ChildNodes)
            {
                XmlNode result = GetNodeFromCount(count, ref current, childNode);
                if (result != null)
                {
                    return result;
                }
            }
            return null;
        }

        /// <summary>
        /// This will create the requied node name for a Xpath query, including the 
        /// NameSpace prefix
        /// </summary>
        /// <param name="current">Node to get name</param>
        /// <returns>ns:NodeName</returns>
        private string GetNodeWithPrefix(XmlNode current)
        {
            string prefix = xmlSettings.NsMgr.LookupPrefix(current.NamespaceURI);
            if (prefix != string.Empty)
                prefix += ":";
            else
                prefix = "";
            return prefix + current.Name;
        }

        /// <summary>
        /// Generates the position part for the Xpath
        /// </summary>
        /// <param name="current">node to get the position for</param>
        /// <returns>[N] or nothing.</returns>
        private string GetXPathPosition(XmlNode current)
        {
            int pos = GetNodePosition(current);
            return pos > 0 ? string.Format("[{0}]", pos) : "";
        }

        /// <summary>
        /// This generates the attribute part for a Xpath
        /// </summary>
        /// <param name="current">node to get attribtes for</param>
        /// <returns>[@Attribute="value" and ......]</returns>
        private string GetXPathAttributes(XmlNode current)
        {
            //do not get the attributes for the Document element.
            if (current == current.OwnerDocument.DocumentElement)
                return "";

            string idNode = xmlSettings.IdNode;
            StringBuilder attrs = new StringBuilder("[");
            foreach (XmlAttribute attr in current.Attributes)
            {
                //do not process any namespace declarations
                if (attr.Prefix == "xmlns" || attr.LocalName == "xmlns" || attr.Prefix == "xsi")
                    continue;
                //add the attribute
                if (attrs.Length > 1)
                    attrs.Append(" and ");
                attrs.AppendFormat("@{0}=\"{1}\"", attr.LocalName, attr.Value);
            }

            //if the node is a Object node, see if it has an ID value
            if (IsObject(current) && !string.IsNullOrEmpty(idNode))
            {

                XmlNode id = GetChildElement(current, idNode);
                if (id != null)
                {
                    //add the text to any previous attributes
                    if (attrs.Length > 1)
                        attrs.Append(" and ");

                    //append the ID.
                    attrs.AppendFormat("{0}=\"{1}\"", id.LocalName, id.InnerText);
                }
            }

            attrs.Append("]");

            if (attrs.Length > 2)
                return attrs.ToString();
            return "";
        }


        /// <summary>
        /// Saves a XmlDocuemtn to a file, applying indentation and UTF-8 encoding
        /// </summary>
        /// <param name="doc">document to save</param>
        /// <param name="fileName">the file name to save (include the path)</param>
        public static void SaveToFile(XmlDocument doc, string fileName)
        {
            XmlWriterSettings settings = new XmlWriterSettings();
            settings.Indent = true;
            settings.Encoding = Encoding.UTF8;
            using (XmlWriter writer = XmlWriter.Create(fileName, settings))
            {
                doc.WriteTo(writer);
            }
        }

        /// <summary>
        /// outputs the DOC to a UTF8 formatted string.
        /// </summary>
        /// <param name="doc">XMLdoc to convert</param>
        /// <returns>a xml doc in a string, includes the hearder and indentation.</returns>
        public static string WriteToString(XmlDocument doc)
        {
            try
            {
                string output = null;
                //writer (output stream)
                StringBuilder sb = new StringBuilder(doc.InnerXml.Length + 50);

                using (TextWriter textWriter = new StringWriter(sb))
                {
                    XmlTextWriter writer = new XmlTextWriter(textWriter);//(s, Encoding.UTF8);
                    writer.Formatting = Formatting.Indented;

                    //if (doc.FirstChild.GetType() != typeof(XmlDeclaration))
                    //    writer.WriteStartDocument();

                    doc.WriteTo(writer);
                    writer.Flush();
                    output = sb.ToString();//Encoding.UTF8.GetString(s.ToArray());
                }
                return output;
            }
            catch (Exception ex)
            {
                throw new Exception("cannot write the XML to string", ex);
            }
        }
    }

}
